package dolmisani.toys.emulators.j2600.cart;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import jstella.cart.Cartridge3F;
import jstella.cart.CartridgeDPC;
import jstella.cart.CartridgeE0;
import jstella.cart.CartridgeE7;
import jstella.cart.CartridgeF4;
import jstella.cart.CartridgeF4SC;
import jstella.cart.CartridgeF6;
import jstella.cart.CartridgeF6SC;
import jstella.cart.CartridgeF8;
import jstella.cart.CartridgeF8SC;
import jstella.cart.CartridgeFASC;
import jstella.cart.CartridgeFE;
import jstella.core.JSException;

/**
 * This class creates Cartridge object starting from ROM image dump. The
 * Cartridge type shall be known in order to build the proper Cartridge object
 *
 * @author Daniele Olmisani <daniele.olmisani@gmail.com>
 */
public class CartridgeFactory {

    /*
     * TODO: insert logging system
     */
    public final static String TYPE_2K = "2K";
    public final static String TYPE_4K = "4K";
    public final static String TYPE_F8 = "F8";
    public final static String TYPE_F8SWAPPED = "F8 swapped";
    public final static String TYPE_F8SC = "F8SC";
    public final static String TYPE_FASC = "FASC";
    public final static String TYPE_F6 = "F6";
    public final static String TYPE_F6SC = "F6SC";
    public final static String TYPE_F4 = "F4";
    public final static String TYPE_FE = "FE";
    public final static String TYPE_DPC = "DPC";
    public final static String TYPE_E0 = "E0";
    public final static String TYPE_E7 = "E7";
    public final static String TYPE_3F = "3F";
    public final static String TYPE_F4SC = "F4SC";

    public CartridgeFactory() {
        //nothing to do
    }

    public static Cartridge create(byte[] imageBytes) throws JSException {

        String md5Digest = calculateMD5Digest(imageBytes);
        System.out.println("LOADING ROM : " + md5Digest);

        String imageType = detectTypeByMD5Digest(md5Digest);
        if (imageType == null) {
            imageType = detectTypeByImage(imageBytes);
        }

        System.out.println("Detected cartridge type: " + imageType);

        // We should know the cart's zType by now so let's create it
        Cartridge cartridge = create(imageBytes, imageType);

        return cartridge;
    }

    public static Cartridge create(byte[] imageBytes, String imageType) throws JSException {

        switch (imageType.toUpperCase()) {
            case TYPE_2K:
                return new Cartridge2K(imageBytes);
            case TYPE_4K:
                return new Cartridge4K(imageBytes);
            case TYPE_F8:
                return new CartridgeF8(imageBytes, false);
            case TYPE_F8SWAPPED:
                return new CartridgeF8(imageBytes, true);
            case TYPE_F8SC:
                return new CartridgeF8SC(imageBytes);
            case TYPE_F6:
                return new CartridgeF6(imageBytes);
            case TYPE_F6SC:
                return new CartridgeF6SC(imageBytes);
            case TYPE_F4:
                return new CartridgeF4(imageBytes);
            case TYPE_F4SC:
                return new CartridgeF4SC(imageBytes);
            case TYPE_FE:
                return new CartridgeFE(imageBytes);
            case TYPE_DPC:
                return new CartridgeDPC(imageBytes);
            case TYPE_E0:
                return new CartridgeE0(imageBytes);
            case TYPE_E7:
                return new CartridgeE7(imageBytes);
            case TYPE_3F:
                return new Cartridge3F(imageBytes);
            case TYPE_FASC:
                return new CartridgeFASC(imageBytes);
            default:
                throw new JSException(
                        JSException.ExceptionType.CART_NOT_SUPPORTED,
                        String.format("NOT Supported '%s' cartridge type", imageType));
        }
    }

    public static String calculateMD5Digest(byte[] bytes) {

        String result = null;

        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");

            messageDigest.update(bytes);
            byte[] theDigest = messageDigest.digest();

            BigInteger digestValue = new BigInteger(1, theDigest);
            result = digestValue.toString(16);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }

        return result;
    }

    private static String detectTypeByMD5Digest(String md5Digest) {

        md5Digest = md5Digest.toLowerCase().trim();

        switch (md5Digest) {
            case "bc24440b59092559a1ec26055fd1270e":
                return TYPE_F8SWAPPED;
            case "75ee371ccfc4f43e7d9b8f24e1266b55":
                return TYPE_F8SWAPPED;
            case "6dda84fb8e442ecf34241ac0d1d91d69":
                return TYPE_F6SC;
        }

        return null;
    }

    private static String detectTypeByImage(byte[] imageBytes) {

        // Guess type based on size
        String type = "";

        int size = imageBytes.length;

        if ((size % 8448) == 0) {
            type = "AR";
        } else if ((size == 2048)
                || ((size == 4096) && (arrayRegionCompare(imageBytes, 0, 2048, 2048) == true))) {
            if (isProbablyCV(imageBytes, size)) {
                type = "CV";
            } else {
                type = TYPE_2K;
            }
        } else if (size == 4096) {
            if (isProbablyCV(imageBytes, size)) {
                type = "CV";
            } else {
                type = TYPE_4K;
            }
        } else if (size == 8192) // 8K
        {
            if (isProbablySC(imageBytes, size)) {
                type = TYPE_F8SC;
            } else if (arrayRegionCompare(imageBytes, 0, 4096, 4096) == true) {
                type = TYPE_4K;
            } else if (isProbablyE0(imageBytes, size)) {
                type = TYPE_E0;
            } else if (isProbably3E(imageBytes, size)) {
                type = "3E";
            } else if (isProbably3F(imageBytes, size)) {
                type = TYPE_3F;
            } else if (isProbablyUA(imageBytes, size)) {
                type = "UA";
            } else if (isProbablyFE(imageBytes, size)) {
                type = TYPE_FE;
            } else {
                type = TYPE_F8;
            }
        } else if ((size == 10495) || (size == 10496) || (size == 10240)) // 10K - Pitfall2
        {
            type = TYPE_DPC;
        } else if (size == 12288) // 12K
        {
            // TODO - this should really be in a method that checks the first
            // 512 bytes of ROM and finds if either the lower 256 bytes or
            // higher 256 bytes are all the same.  For now, we assume that
            // all carts of 12K are CBS RAM Plus/FASC.
            type = "FASC";
        } else if (size == 16384) // 16K
        {
            if (isProbablySC(imageBytes, size)) {
                type = TYPE_F6SC;
            } else if (isProbablyE7(imageBytes, size)) {
                type = TYPE_E7;
            } else if (isProbably3E(imageBytes, size)) {
                type = "3E";
            } else if (isProbably3F(imageBytes, size)) {
                type = TYPE_3F;
            } else {
                type = TYPE_F6;
            }
        } else if (size == 32768) // 32K
        {
            if (isProbablySC(imageBytes, size)) {
                type = TYPE_F4SC;
            } else if (isProbably3E(imageBytes, size)) {
                type = "3E";
            } else if (isProbably3F(imageBytes, size)) {
                type = TYPE_3F;
            } else {
                type = "F4";
            }
        } else if (size == 65536) // 64K
        {
            // TODO - autodetect 4A50
            if (isProbably3E(imageBytes, size)) {
                type = "3E";
            } else if (isProbably3F(imageBytes, size)) {
                type = TYPE_3F;
            } else {
                type = "MB";
            }
        } else if (size == 131072) // 128K
        {
            // TODO - autodetect 4A50
            if (isProbably3E(imageBytes, size)) {
                type = "3E";
            } else if (isProbably3F(imageBytes, size)) {
                type = TYPE_3F;
            } else {
                type = "MC";
            }
        } else // what else can we do?
        {
            if (isProbably3E(imageBytes, size)) {
                type = "3E";
            } else if (isProbably3F(imageBytes, size)) {
                type = TYPE_3F;
            } else {
                type = TYPE_4K;  // Most common bankswitching type
            }
        }

        return type;
    }

    static boolean isProbablySC(byte[] image, int size) {
        // We assume a Superchip cart contains the same bytes for its entire
        // RAM area; obviously this test will fail if it doesn't
        // The RAM area will be the first 256 bytes of each 4K bank
        int banks = (int) ((double) size / 4096);
        for (int i = 0; i < banks; ++i) {
            byte first = image[i * 4096];
            for (int j = 0; j < 256; ++j) {
                if (image[(i * 4096) + j] != first) {
                    return false;
                }
            }
        }
        return true;
    }

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    static boolean isProbably3F(byte[] image, int size) {
        // 3F cart bankswitching is triggered by storing the bank number
        // in address 3F using 'STA $3F'
        // We expect it will be present at least 2 times, since there are
        // at least two banks
        int[] intsignature = {0x85, 0x3F};  // STA $3F
        byte[] signature = toDataUByte(intsignature);
        return searchForBytes(image, signature, 2, 2);
    }

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    static boolean isProbably3E(byte[] image, int size) {
        // 3E cart bankswitching is triggered by storing the bank number
        // in address 3E using 'STA $3E', commonly followed by an
        // immediate mode LDA
        int[] intsignature = {0x85, 0x3E, 0xA9, 0x00};  // STA $3E; LDA #$00
        byte[] signature = toDataUByte(intsignature);
        return searchForBytes(image, signature, 4, 1);
    }

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    static boolean isProbablyE0(byte[] image, int size) {
        // E0 cart bankswitching is triggered by accessing addresses
        // $FE0 to $FF9 using absolute non-indexed addressing
        // To eliminate false positives (and speed up processing), we
        // search for only certain known signatures
        // Thanks to "stella@casperkitty.com" for this advice
        // These signatures are attributed to the MESS project
        int[][] intsignature = { /*[6][3]*/
            {0x8D, 0xE0, 0x1F}, // STA $1FE0
            {0x8D, 0xE0, 0x5F}, // STA $5FE0
            {0x8D, 0xE9, 0xFF}, // STA $FFE9
            {0xAD, 0xE9, 0xFF}, // LDA $FFE9
            {0xAD, 0xED, 0xFF}, // LDA $FFED
            {0xAD, 0xF3, 0xBF} // LDA $BFF3
        };
        byte[][] signature = toDataUByte(intsignature);
        for (int i = 0; i < 6; ++i) {
            if (searchForBytes(image, signature[i], 3, 1)) {
                return true;
            }
        }
        return false;
    }

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    static boolean isProbablyE7(byte[] image, int size) {
        // E7 carts map their second 1K block of RAM at addresses
        // $800 to $8FF.  However, since this occurs in the upper 2K address
        // space, and the last 2K in the cart always points to the last 2K of the
        // ROM image, the RAM area should fall in addresses $3800 to $38FF
        // Similar to the Superchip cart, we assume this RAM block contains
        // the same bytes for its entire area
        // Also, we want to distinguish between ROMs that have large blocks
        // of the same amount of (probably unused) data by making sure that
        // something differs in the previous 32 or next 32 bytes
        byte first = image[0x3800];
        for (int i = 0x3800; i < 0x3A00; ++i) {
            if (first != image[i]) {
                return false;
            }
        }

        // OK, now scan the surrounding 32 byte blocks
        int count1 = 0, count2 = 0;
        for (int i = 0x3800 - 32; i < 0x3800; ++i) {
            if (first != image[i]) {
                ++count1;
            }
        }
        for (int i = 0x3A00; i < 0x3A00 + 32; ++i) {
            if (first != image[i]) {
                ++count2;
            }
        }

        return (count1 > 0 || count2 > 0);
    }

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    static boolean isProbablyUA(byte[] image, int size) {
        // UA cart bankswitching switches to bank 1 by accessing address 0x240
        // using 'STA $240'
        int[] intsignature = {0x8D, 0x40, 0x02};  // STA $240
        byte[] signature = toDataUByte(intsignature);
        return searchForBytes(image, signature, 3, 1);
    }

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    static boolean isProbablyCV(byte[] image, int size) {
        // CV RAM access occurs at addresses $f3ff and $f400
        // These signatures are attributed to the MESS project
        int[][] intsignature /*[2][3]*/ = {
            {0x9D, 0xFF, 0xF3}, // STA $F3FF
            {0x99, 0x00, 0xF4} // STA $F400
        };
        byte[][] signature = toDataUByte(intsignature);
        if (searchForBytes(image, signature[0], 3, 1)) {
            return true;
        } else {
            return searchForBytes(image, signature[1], 3, 1);
        }
    }

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    static boolean isProbablyFE(byte[] image, int size) {
        // FE bankswitching is very weird, but always seems to include a
        // 'JSR $xxxx'
        // These signatures are attributed to the MESS project
        int[][] intsignature/*[4][5]*/ = {
            {0x20, 0x00, 0xD0, 0xC6, 0xC5}, // JSR $D000; DEC $C5
            {0x20, 0xC3, 0xF8, 0xA5, 0x82}, // JSR $F8C3; LDA $82
            {0xD0, 0xFB, 0x20, 0x73, 0xFE}, // BNE $FB; JSR $FE73
            {0x20, 0x00, 0xF0, 0x84, 0xD6} // JSR $F000; STY $D6
        };
        byte[][] signature = toDataUByte(intsignature);
        for (int i = 0; i < 4; ++i) {
            if (searchForBytes(image, signature[i], 5, 1)) {
                return true;
            }
        }
        return false;
    }

    private static boolean arrayRegionCompare(byte[] bytes, int startA, int startB, int length) {

        for (int i = 0; i < length; i++) {
            if (bytes[startA + i] != bytes[startB + i]) {
                return false;
            }
        }

        return true;
    }

    static boolean searchForBytes(byte[] image, byte[] signature, int sigsize, int minhits) {
        int count = 0;
        int imagesize = image.length;
        for (int i = 0; i < imagesize - sigsize; /*++i*/ i++) {
            int matches = 0;
            for (int j = 0; j < sigsize; /*++j*/ j++) {
                if (image[i + j] == signature[j]) {
                    ++matches;
                } else {
                    break;
                }
            }
            if (matches == sigsize) {
                ++count;
                i += sigsize;  // skip past this signature 'window' entirely
            }
            if (count >= minhits) {
                break;
            }
        }

        return (count >= minhits);
    }

    public static byte toDataUByte(int aByteValue) {
        return (byte) (aByteValue & 0xff);
    }

    /**
     * Performs toDataUByte(int) on an array
     *
     * @param aArray Array of values
     * @return Array of bytes
     */
    public static byte[] toDataUByte(int[] aArray) {
        byte[] zReturn = new byte[aArray.length];
        for (int i = 0; i < aArray.length; i++) {
            zReturn[i] = toDataUByte(aArray[i]);
        }//end : for i loop
        return zReturn;
    }

    /**
     * Performs toDataUByte(int) on a double array
     *
     * @param aArray a double array
     * @return a double array of byte
     */
    public static byte[][] toDataUByte(int[][] aArray) {
        byte[][] zReturn = new byte[aArray.length][];
        for (int i = 0; i < aArray.length; i++) {
            zReturn[i] = new byte[aArray[i].length];
            for (int j = 0; j < aArray[i].length; j++) {
                zReturn[i][j] = toDataUByte(aArray[i][j]);
            }//end : for j loop
        }//end : for i loop
        return zReturn;
    }
}
